Panduan komprehensif untuk generik TypeScript, mencakup sintaks, manfaat, penggunaan lanjutan, dan praktik terbaik untuk menangani tipe data kompleks dalam pengembangan perangkat lunak global.
Generik TypeScript: Menguasai Tipe Data Kompleks untuk Aplikasi yang Tangguh
TypeScript, superset dari JavaScript, memberdayakan pengembang untuk menulis kode yang lebih tangguh dan mudah dipelihara melalui pengetikan statis. Di antara fitur-fiturnya yang paling kuat adalah generik, yang memungkinkan Anda menulis kode yang dapat bekerja dengan berbagai tipe data sambil tetap menjaga keamanan tipe. Panduan ini memberikan eksplorasi komprehensif tentang generik TypeScript, dengan fokus pada penerapannya pada tipe data kompleks dalam konteks pengembangan perangkat lunak global.
Apa itu Generik?
Generik menyediakan cara untuk menulis kode yang dapat digunakan kembali yang dapat bekerja dengan tipe yang berbeda. Alih-alih menulis fungsi atau kelas terpisah untuk setiap tipe yang ingin Anda dukung, Anda dapat menulis satu fungsi atau kelas yang menggunakan parameter tipe. Parameter tipe ini adalah placeholder untuk tipe aktual yang akan digunakan saat fungsi atau kelas dipanggil atau diinstansiasi. Ini sangat berguna ketika berhadapan dengan struktur data kompleks di mana tipe data di dalam struktur tersebut dapat bervariasi.
Manfaat Menggunakan Generik
- Ketergunaan Kembali Kode: Tulis kode sekali dan gunakan dengan tipe yang berbeda. Ini mengurangi duplikasi kode dan membuat basis kode Anda lebih mudah dipelihara.
- Keamanan Tipe: Generik memungkinkan kompiler TypeScript untuk memberlakukan keamanan tipe pada waktu kompilasi. Ini membantu mencegah kesalahan saat runtime yang terkait dengan ketidakcocokan tipe.
- Keterbacaan yang Ditingkatkan: Generik membuat kode Anda lebih mudah dibaca dengan secara jelas menunjukkan tipe yang dirancang untuk bekerja dengan fungsi dan kelas Anda.
- Peningkatan Kinerja: Dalam beberapa kasus, generik dapat menghasilkan peningkatan kinerja karena kompiler dapat mengoptimalkan kode yang dihasilkan berdasarkan tipe spesifik yang digunakan.
Sintaks Dasar Generik
Sintaks dasar generik melibatkan penggunaan kurung sudut (< >) untuk mendeklarasikan parameter tipe. Parameter tipe ini biasanya dinamai T
, K
, V
, dll., tetapi Anda dapat menggunakan pengidentifikasi yang valid. Berikut adalah contoh sederhana dari fungsi generik:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Output: hello
console.log(myNumber); // Output: 123
console.log(myBoolean); // Output: true
Dalam contoh ini, <T>
mendeklarasikan parameter tipe bernama T
. Fungsi identity
mengambil argumen tipe T
dan mengembalikan nilai tipe T
. Saat memanggil fungsi, Anda dapat secara eksplisit menentukan parameter tipe (misalnya, identity<string>
) atau membiarkan TypeScript menyimpulkannya berdasarkan tipe argumen.
Bekerja dengan Tipe Data Kompleks
Generik menjadi sangat berharga ketika berhadapan dengan tipe data kompleks seperti array, objek, dan antarmuka. Mari kita jelajahi beberapa skenario umum:
Array Generik
Anda dapat menggunakan generik untuk membuat fungsi atau kelas yang bekerja dengan array dari berbagai tipe:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Output: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Output: apple, banana, cherry
Di sini, fungsi arrayToString
mengambil array tipe T[]
dan mengembalikan representasi string dari array tersebut. Fungsi ini bekerja dengan array dari tipe apa pun, membuatnya sangat dapat digunakan kembali.
Objek Generik
Generik juga dapat digunakan untuk mendefinisikan fungsi atau kelas yang bekerja dengan objek dari berbagai bentuk:
interface Person {
name: string;
age: number;
country: string; // Added country for global context
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Added currency for global context
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Output: Name: Alice
displayInfo(product); // Output: Name: Laptop
Dalam contoh ini, fungsi displayInfo
mengambil objek tipe T
yang harus memiliki properti name
bertipe string. Klausa extends { name: string }
adalah batasan (constraint), yang menentukan persyaratan minimum untuk parameter tipe T
. Ini memastikan bahwa fungsi dapat dengan aman mengakses properti name
.
Penggunaan Generik Tingkat Lanjut
Generik TypeScript menawarkan fitur yang lebih canggih yang memungkinkan Anda membuat kode yang lebih fleksibel dan kuat. Mari kita jelajahi beberapa fitur ini:
Parameter Tipe Ganda
Anda dapat mendefinisikan fungsi atau kelas dengan beberapa parameter tipe:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Output: Bob
console.log(merged.age); // Output: 42
Fungsi merge
mengambil dua objek tipe T
dan U
dan mengembalikan objek baru yang berisi properti dari kedua objek tersebut. Ini adalah cara yang kuat untuk menggabungkan data dari sumber yang berbeda.
Batasan Generik
Seperti yang ditunjukkan sebelumnya, batasan memungkinkan Anda untuk membatasi tipe yang dapat digunakan dengan parameter tipe generik. Ini memastikan bahwa kode generik dapat beroperasi dengan aman pada tipe yang ditentukan.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Output: 3
loggingIdentity("hello"); // Output: 5
// loggingIdentity(123); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Fungsi loggingIdentity
mengambil argumen tipe T
yang harus memiliki properti length
bertipe number. Ini memastikan bahwa fungsi dapat dengan aman mengakses properti length
.
Kelas Generik
Generik juga dapat digunakan dengan kelas:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Output: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Output: [ 2 ]
Kelas DataStorage
dapat menyimpan data dari tipe apa pun T
. Ini memungkinkan Anda untuk membuat struktur data yang dapat digunakan kembali yang aman tipe.
Antarmuka Generik
Antarmuka generik berguna untuk mendefinisikan kontrak yang dapat bekerja dengan berbagai tipe. Sebagai contoh:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Antarmuka Result
mendefinisikan struktur generik untuk merepresentasikan hasil dari suatu operasi. Ini bisa berisi data tipe T
atau kesalahan tipe E
. Ini adalah pola umum untuk menangani operasi asinkron atau operasi yang mungkin gagal.
Tipe Utilitas dan Generik
TypeScript menyediakan beberapa tipe utilitas bawaan yang bekerja dengan baik dengan generik. Tipe utilitas ini dapat membantu Anda mengubah dan memanipulasi tipe dengan cara yang kuat.
Partial<T>
Partial<T>
membuat semua properti dari tipe T
menjadi opsional:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Valid
Readonly<T>
Readonly<T>
membuat semua properti dari tipe T
menjadi readonly:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Error: Cannot assign to 'age' because it is a read-only property.
Pick<T, K>
Pick<T, K>
memilih satu set properti K
dari tipe T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
menghapus satu set properti K
dari tipe T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
membuat tipe dengan kunci K
dan nilai bertipe T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Expanded list for global context
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Expanded list for global context
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Tipe Terpetakan (Mapped Types)
Tipe terpetakan memungkinkan Anda untuk mengubah tipe yang ada dengan melakukan iterasi pada propertinya. Ini adalah cara yang kuat untuk membuat tipe baru berdasarkan yang sudah ada. Sebagai contoh, Anda dapat membuat tipe yang membuat semua properti dari tipe lain menjadi readonly:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Error: Cannot assign to 'age' because it is a read-only property.
Dalam contoh ini, [K in keyof Person]
melakukan iterasi pada semua kunci dari antarmuka Person
, dan Person[K]
mengakses tipe dari setiap properti. Kata kunci readonly
membuat setiap properti menjadi readonly.
Tipe Kondisional
Tipe kondisional memungkinkan Anda untuk mendefinisikan tipe berdasarkan kondisi. Ini adalah cara yang kuat untuk membuat tipe yang beradaptasi dengan skenario yang berbeda.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Handles both null and undefined
throw new Error("Value cannot be null or undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Output: HELLO
const invalidValue = getValue(null); // This will throw an error
console.log(invalidValue); // This line will not be reached
} catch (error: any) {
console.error(error.message); // Output: Value cannot be null or undefined
}
Dalam contoh ini, tipe NonNullable<T>
memeriksa apakah T
adalah null
atau undefined
. Jika ya, ia mengembalikan never
, yang berarti tipe tersebut tidak diizinkan. Jika tidak, ia mengembalikan T
. Ini memungkinkan Anda untuk membuat tipe yang dijamin tidak bisa null.
Praktik Terbaik Menggunakan Generik
Berikut adalah beberapa praktik terbaik yang perlu diingat saat menggunakan generik:
- Gunakan nama parameter tipe yang deskriptif: Pilih nama yang secara jelas menunjukkan tujuan dari parameter tipe.
- Gunakan batasan untuk membatasi tipe yang dapat digunakan dengan parameter tipe generik: Ini memastikan bahwa kode generik Anda dapat beroperasi dengan aman pada tipe yang ditentukan.
- Jaga agar kode generik Anda tetap sederhana dan fokus: Hindari membuat kode generik Anda terlalu rumit dengan terlalu banyak parameter tipe atau batasan yang kompleks.
- Dokumentasikan kode generik Anda secara menyeluruh: Jelaskan tujuan dari parameter tipe dan batasan apa pun yang digunakan.
- Pertimbangkan trade-off antara ketergunaan kembali kode dan keamanan tipe: Meskipun generik dapat meningkatkan ketergunaan kembali kode, mereka juga dapat membuat kode Anda lebih kompleks. Timbang manfaat dan kerugiannya sebelum menggunakan generik.
- Pertimbangkan lokalisasi dan globalisasi (l10n dan g11n): Ketika berhadapan dengan data yang perlu ditampilkan kepada pengguna di berbagai wilayah, pastikan generik Anda mendukung pemformatan dan konvensi budaya yang sesuai. Misalnya, pemformatan angka dan tanggal dapat sangat bervariasi di berbagai lokal.
Contoh dalam Konteks Global
Mari kita pertimbangkan beberapa contoh bagaimana generik dapat digunakan dalam konteks global:
Konversi Mata Uang
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Output: 100 USD is equal to 85 EUR
Pemformatan Tanggal
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));
Layanan Terjemahan
interface Translation {
[key: string]: string; // Allows for dynamic language keys
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Translation for ${key} in ${languageCode} not found.`;
}
return lang.translations[key] || `Translation for ${key} not found.`;
}
console.log(translate("hello", "en", languageData)); // Output: Hello
console.log(translate("hello", "es", languageData)); // Output: Hola
console.log(translate("welcome", "fr", languageData)); // Output: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Output: Translation for missingKey in de not found.
Kesimpulan
Generik TypeScript adalah alat yang ampuh untuk menulis kode yang dapat digunakan kembali, aman tipe, yang dapat bekerja dengan tipe data kompleks. Dengan memahami sintaks dasar, fitur lanjutan, dan praktik terbaik generik, Anda dapat secara signifikan meningkatkan kualitas dan kemudahan pemeliharaan aplikasi TypeScript Anda. Saat mengembangkan aplikasi untuk audiens global, generik dapat membantu Anda menangani format data dan konvensi budaya yang beragam, memastikan pengalaman pengguna yang mulus untuk semua orang.